using System;
using System.Linq.Expressions;
using System.Reflection;

namespace RedRiver.Framework.XtraCommon.FastReflection{
    public interface IPropertyAccessor{
        object GetValue(object instance);

        void SetValue(object instance, object value);
    }

    public class PropertyAccessor : IPropertyAccessor{
        private Func<object, object> _getter;
        private MethodInvoker _setMethodInvoker;

        public PropertyAccessor(PropertyInfo propertyInfo){
            PropertyInfo = propertyInfo;
            InitializeGet(propertyInfo);
            InitializeSet(propertyInfo);
        }

        public PropertyInfo PropertyInfo{
            get;
            private set;
        }

        #region IPropertyAccessor Members

        object IPropertyAccessor.GetValue(object instance){
            return GetValue(instance);
        }

        void IPropertyAccessor.SetValue(object instance, object value){
            SetValue(instance, value);
        }

        #endregion

        private void InitializeGet(PropertyInfo propertyInfo){
            if (!propertyInfo.CanRead){
                return;
            }

            // Target: (object)(((TInstance)instance).Property)

            // preparing parameter, object type
            ParameterExpression instance = Expression.Parameter(typeof (object), "instance");

            // non-instance for static method, or ((TInstance)instance)
            UnaryExpression instanceCast = propertyInfo.GetGetMethod(true).IsStatic
                                               ? null
                                               :
                                                   Expression.Convert(instance, propertyInfo.ReflectedType);

            // ((TInstance)instance).Property
            MemberExpression propertyAccess = Expression.Property(instanceCast, propertyInfo);

            // (object)(((TInstance)instance).Property)
            UnaryExpression castPropertyValue = Expression.Convert(propertyAccess, typeof (object));

            // Lambda expression
            Expression<Func<object, object>> lambda = Expression.Lambda<Func<object, object>>(castPropertyValue,
                                                                                              instance);

            _getter = lambda.Compile();
        }

        private void InitializeSet(PropertyInfo propertyInfo){
            if (!propertyInfo.CanWrite){
                return;
            }
            _setMethodInvoker = new MethodInvoker(propertyInfo.GetSetMethod(true));
        }

        public object GetValue(object o){
            if (_getter == null){
                throw new NotSupportedException("Get method is not defined for this property.");
            }

            return _getter(o);
        }

        public void SetValue(object o, object value){
            if (_setMethodInvoker == null){
                throw new NotSupportedException("Set method is not defined for this property.");
            }

            _setMethodInvoker.Invoke(o, new[]{value});
        }
    }
}